Design a Payment Gateway

Ashish

Ashish Pratap Singh

medium

In this chapter, we will explore the low-level design of a Payment Gateway.

Let’s start by clarifying the requirements:

1. Clarifying Requirements

Designing a payment gateway involves many moving parts. Before diving into the implementation, it's critical to clarify the scope and constraints of the system we are expected to design.

After gathering the details, we can summarize the key system requirements.

1.1 Functional Requirements

  • The system supports multiple payment methods, including Credit Card, PayPal, and UPI.
  • The system processes the request using the appropriate payment processor.
  • If processing fails, the system retries the request up to a maximum number of times (e.g., 3).
  • The system should notify interested parties (e.g., customer, merchant) upon payment status updates.

1.2 Non-Functional Requirements

  • The design should follow object-oriented principles and be modular to support future extensions (e.g., new payment methods).
  • The system should be testable, extensible, and simulate real-world payment flows without actual external API calls.

After the requirements are clear, the next step is to identify the core entities that we will form the foundation of our design.

2. Identifying Core Entities

Core entities are the fundamental building blocks of our system. We identify them by analyzing the functional requirements and highlighting the key nouns and responsibilities that naturally map to object-oriented abstractions such as classes, enums, or interfaces.

Let’s walk through the functional requirements and extract the relevant entities:

The system must accept payment requests from merchants.

A merchant's request to initiate a payment will contain various pieces of information, such as the amount, currency, and customer details. This suggests the need for a PaymentRequest entity to encapsulate all this incoming data into a single object. Correspondingly, the system must provide an immediate result of the processing attempt, leading to a PaymentResponse entity to hold the status and a message.

The system must support multiple payment methods, such as Credit Card, PayPal, and UPI.

The different payment methods can be represented by a PaymentMethod enum, ensuring type safety. Since the logic for processing a payment is different for each method, we need a common abstraction. This points to a PaymentProcessor interface (Strategy Pattern), which defines a standard processPayment method. We will then have concrete implementations like CreditCardProcessorPayPalProcessor, and UPIProcessor, each handling the specifics of its method.

The system must create, track, and log every payment attempt.

Each payment request should be treated as a unique Transaction. This entity will serve as the central record, holding the original PaymentRequest and tracking its lifecycle. The state of this lifecycle (e.g., INITIATED, SUCCESSFUL, FAILED) can be modeled with a PaymentStatus enum.

The gateway must be able to select the correct payment processor dynamically.

To avoid coupling the main service with the creation logic of concrete processors, we can introduce a PaymentProcessorFactory. This factory's sole responsibility will be to instantiate and return the appropriate PaymentProcessor based on the PaymentMethod specified in the request.

When a transaction's status changes, other systems (e.g., merchant backend, customer notification service) must be informed.

This is an event-driven requirement, best solved with the Observer pattern. We'll define a PaymentObserver interface for any component that needs to react to transaction updates. Concrete implementations, such as CustomerNotifier and MerchantNotifier, can then subscribe to receive these updates.

The system must provide a simple, unified interface for merchants to integrate with.

To hide the internal complexity of factories, processors, and observers, we need a single, easy-to-use entry point. A PaymentGatewayService will act as a Facade, providing a clean API for merchants to process payments and abstracting away the underlying orchestration.

3. Designing Classes and Relationships

This section details the design of each class identified previously, including their specific attributes and methods. We will also illustrate how these classes relate to one another and highlight the key design patterns that underpin our solution.

3.1 Class Definitions

We can categorize our classes into enums, data-holding classes, and core classes that encapsulate the system's primary logic.

Enums

Enums

PaymentMethod

A type-safe enumeration to represent the different payment instruments supported by the gateway.

  • Values: CREDIT_CARD, PAYPAL, UPI.

PaymentStatus

A type-safe enumeration to represent the distinct states a transaction can be in throughout its lifecycle.

  • Values: INITIATED, SUCCESSFUL, FAILED.

Data Classes

PaymentRequest

 A data transfer object (DTO) that encapsulates all the necessary information from a merchant to initiate a payment. It is constructed using the Builder pattern for flexibility and readability.

PaymentRequest
  • Attributes: transactionId, payerId, amount, currency, paymentMethod, paymentDetails (a map for method-specific data like card numbers).

PaymentResponse

A simple DTO that carries the immediate synchronous result of a payment processing attempt back to the caller.

PaymentResponse
  • Attributes: status (PaymentStatus), message (String).

Transaction

 The central domain object representing a single payment from start to finish. It links the initial request with its evolving status, serving as a single source of truth for that payment's history.

Transaction
  • Attributes: id, request (PaymentRequest), status (PaymentStatus), timestamp.
  • Methods: setStatus(PaymentStatus status).

Core Classes

PaymentGatewayService

Acts as the system's Facade and Singleton entry point. It orchestrates the entire payment flow: receiving a request, using the factory to get a processor, executing the payment, updating the transaction status, and notifying all registered observers.

PaymentGatewayService
  • Attributes: instance (for Singleton), observers (a list of PaymentObserver).
  • Methods: getInstance(), addObserver(), processPayment(), notifyObservers().

PaymentProcessor

PaymentProcessor

3.2 Class Relationships

Implementation

  • CreditCardProcessor, PayPalProcessor, and UPIProcessor extend the AbstractPaymentProcessor.
  • AbstractPaymentProcessor implements the PaymentProcessor interface.
  • CustomerNotifier and MerchantNotifier implement the PaymentObserver interface.

Composition / Aggregation

  • Transaction has a PaymentRequest.
  • PaymentGatewayService has a list of PaymentObservers.

Dependency / "Uses-a

  • PaymentGatewayService uses PaymentProcessorFactory to obtain a PaymentProcessor.
  • PaymentGatewayService uses the PaymentProcessor to process the payment.
  • PaymentGatewayService creates and manages Transaction objects.
  • Concrete PaymentProcessors create PaymentResponse objects.

3.3 Key Design Patterns

Strategy Pattern

The PaymentProcessor interface and its concrete implementations (CreditCardProcessor, PayPalProcessor, etc.) embody this pattern. Each processor is a different "strategy" for handling a payment. This allows the system to easily support new payment methods by simply adding a new processor class.

Observer Pattern

The PaymentObserver interface, concrete observers (CustomerNotifier, MerchantNotifier), and the PaymentGatewayService (as the subject) form a classic Observer pattern. This allows different parts of the system to react to transaction status changes without being tightly coupled to the payment processing logic.

PaymentObserver

Factory Pattern (Simple Factory)

The PaymentProcessorFactory centralizes the creation logic for PaymentProcessor objects. This decouples the client (PaymentGatewayService) from the concrete processor classes, making the system more flexible and adhering to the open/closed principle.

Builder Pattern

The PaymentRequest.Builder provides a clean and fluent API for constructing a PaymentRequest object, which has multiple parameters. This improves readability and is more flexible than using telescoping constructors.

Builder

Template Method Pattern

The AbstractPaymentProcessor uses this pattern to define a skeleton algorithm for processing a payment (including retries) while allowing subclasses to override the specific doProcess step. This avoids code duplication (retry logic) across different processors.

Facade Pattern

The PaymentGatewayService serves as a Facade. It provides a single, simplified interface for merchants to interact with, hiding the complex internal machinery of factories, processors, transactions, and observers.

3.4 Full Class Diagram

Class Diagram

4. Implementation

4.1 Enums

1class PaymentMethod(Enum):
2    CREDIT_CARD = "CREDIT_CARD"
3    PAYPAL = "PAYPAL"
4    UPI = "UPI"
5
6class PaymentStatus(Enum):
7    INITIATED = "INITIATED"
8    SUCCESSFUL = "SUCCESSFUL"
9    FAILED = "FAILED"

4.2 PaymentRequest

1class PaymentRequest:
2    def __init__(self, builder):
3        self.transaction_id = str(uuid.uuid4())
4        self.payer_id = builder.payer_id
5        self.amount = builder.amount
6        self.currency = builder.currency
7        self.payment_method = builder.payment_method
8        self.payment_details = builder.payment_details
9
10    def get_transaction_id(self) -> str:
11        return self.transaction_id
12
13    def get_amount(self) -> float:
14        return self.amount
15
16    def get_currency(self) -> str:
17        return self.currency
18
19    def get_payment_method(self) -> PaymentMethod:
20        return self.payment_method
21
22    class Builder:
23        def __init__(self):
24            self.payer_id = None
25            self.amount = None
26            self.currency = None
27            self.payment_method = None
28            self.payment_details = None
29
30        def payer_id(self, payer_id: str):
31            self.payer_id = payer_id
32            return self
33
34        def amount(self, amount: float):
35            self.amount = amount
36            return self
37
38        def currency(self, currency: str):
39            self.currency = currency
40            return self
41
42        def payment_method(self, payment_method: PaymentMethod):
43            self.payment_method = payment_method
44            return self
45
46        def payment_details(self, payment_details: Dict[str, str]):
47            self.payment_details = payment_details
48            return self
49
50        def build(self) -> 'PaymentRequest':
51            return PaymentRequest(self)

4.3 PaymentResponse

1class PaymentResponse:
2    def __init__(self, status: PaymentStatus, message: str):
3        self.status = status
4        self.message = message
5
6    def get_status(self) -> PaymentStatus:
7        return self.status
8
9    def get_message(self) -> str:
10        return self.message

4.4 PaymentResponse

1class Transaction:
2    def __init__(self, request: PaymentRequest):
3        self.id = request.get_transaction_id()
4        self.request = request
5        self.status = PaymentStatus.INITIATED
6        self.timestamp = datetime.now()
7
8    def set_status(self, status: PaymentStatus) -> None:
9        self.status = status
10
11    def get_id(self) -> str:
12        return self.id
13
14    def get_status(self) -> PaymentStatus:
15        return self.status
16
17    def get_request(self) -> PaymentRequest:
18        return self.request

4.5 PaymentObserver

1class PaymentObserver(ABC):
2    @abstractmethod
3    def on_transaction_update(self, transaction: Transaction) -> None:
4        pass
5
6class CustomerNotifier(PaymentObserver):
7    def on_transaction_update(self, transaction: Transaction) -> None:
8        if transaction.get_status() == PaymentStatus.SUCCESSFUL:
9            print("--- CUSTOMER EMAIL ---")
10            print(f"Your payment of {transaction.get_request().get_amount()} was successful. Transaction ID: {transaction.get_id()}")
11            print("----------------------")
12
13class MerchantNotifier(PaymentObserver):
14    def on_transaction_update(self, transaction: Transaction) -> None:
15        print("--- MERCHANT NOTIFICATION ---")
16        print(f"Transaction {transaction.get_id()} status updated to: {transaction.get_status()}")
17        print("-----------------------------")

4.6 PaymentProcessor

1class PaymentProcessor(ABC):
2    @abstractmethod
3    def process_payment(self, request: PaymentRequest) -> PaymentResponse:
4        pass
5
6class AbstractPaymentProcessor(PaymentProcessor):
7    MAX_RETRIES = 3
8
9    def process_payment(self, request: PaymentRequest) -> PaymentResponse:
10        attempts = 0
11        while attempts < self.MAX_RETRIES:
12            response = self.do_process(request)
13            attempts += 1
14            if response.get_status() != PaymentStatus.FAILED:
15                break
16        return response
17
18    @abstractmethod
19    def do_process(self, request: PaymentRequest) -> PaymentResponse:
20        pass

4.7 PaymentProcessor Implementation

1class CreditCardProcessor(AbstractPaymentProcessor):
2    def do_process(self, request: PaymentRequest) -> PaymentResponse:
3        print(f"Processing credit card payment of amount {request.get_amount()} {request.get_currency()}")
4        # Simulate interaction with Visa/Mastercard network
5        return PaymentResponse(PaymentStatus.SUCCESSFUL, "Credit Card payment successful.")
6
7class PayPalProcessor(AbstractPaymentProcessor):
8    def do_process(self, request: PaymentRequest) -> PaymentResponse:
9        print(f"Redirecting to PayPal for transaction {request.get_transaction_id()}")
10        # Simulate PayPal API interaction
11        return PaymentResponse(PaymentStatus.SUCCESSFUL, "Paypal payment successful.")
12
13class UPIProcessor(AbstractPaymentProcessor):
14    def do_process(self, request: PaymentRequest) -> PaymentResponse:
15        print(f"Processing UPI payment of {request.get_amount()} {request.get_currency()}")
16        return PaymentResponse(PaymentStatus.SUCCESSFUL, "UPI payment successful.")

4.8 PaymentProcessorFactory

1class PaymentProcessorFactory:
2    @staticmethod
3    def get_processor(method: PaymentMethod) -> PaymentProcessor:
4        if method == PaymentMethod.CREDIT_CARD:
5            return CreditCardProcessor()
6        elif method == PaymentMethod.UPI:
7            return UPIProcessor()
8        elif method == PaymentMethod.PAYPAL:
9            return PayPalProcessor()
10        else:
11            raise ValueError(f"Unsupported payment method: {method}")

4.9 PaymentGatewayService

1class PaymentGatewayService:
2    _instance = None
3    _lock = Lock()
4
5    def __new__(cls):
6        if cls._instance is None:
7            with cls._lock:
8                if cls._instance is None:
9                    cls._instance = super().__new__(cls)
10                    cls._instance.observers = []
11        return cls._instance
12
13    @classmethod
14    def get_instance(cls):
15        return cls()
16
17    def add_observer(self, observer: PaymentObserver) -> None:
18        self.observers.append(observer)
19
20    def remove_observer(self, observer: PaymentObserver) -> None:
21        if observer in self.observers:
22            self.observers.remove(observer)
23
24    def _notify_observers(self, transaction: Transaction) -> None:
25        for observer in self.observers:
26            observer.on_transaction_update(transaction)
27
28    def process_payment(self, request: PaymentRequest) -> Transaction:
29        transaction = Transaction(request)
30        try:
31            processor = PaymentProcessorFactory.get_processor(request.get_payment_method())
32            response = processor.process_payment(request)
33            transaction.set_status(response.get_status())
34        except Exception as e:
35            print(f"Payment processing failed: {e}")
36            transaction.set_status(PaymentStatus.FAILED)
37        
38        self._notify_observers(transaction)
39        return transaction

4.10 PaymentGatewayDemo

1def main():
2    # 1. Setup the gateway facade
3    payment_gateway = PaymentGatewayService.get_instance()
4
5    # 2. Register observers to be notified of transaction events
6    payment_gateway.add_observer(MerchantNotifier())
7    payment_gateway.add_observer(CustomerNotifier())
8
9    print("----------- SCENARIO 1: Successful Credit Card Payment -----------")
10    # a. Merchant's backend creates a payment request
11    cc_request = (PaymentRequest.Builder()
12                  .payer_id("U-123")
13                  .amount(150.75)
14                  .currency("INR")
15                  .payment_method(PaymentMethod.CREDIT_CARD)
16                  .payment_details({"cardNumber": "1234..."})
17                  .build())
18
19    # b. Merchant's backend sends it to the facade
20    payment_gateway.process_payment(cc_request)
21
22    print("\n----------- SCENARIO 2: Successful PayPal Payment -----------")
23    paypal_request = (PaymentRequest.Builder()
24                      .payer_id("U-456")
25                      .amount(88.50)
26                      .currency("USD")
27                      .payment_method(PaymentMethod.PAYPAL)
28                      .payment_details({"email": "[email protected]"})
29                      .build())
30
31    payment_gateway.process_payment(paypal_request)
32
33if __name__ == "__main__":
34    main()

5. Run and Test

Languages
Java
C#
Python
C++
Files16
entities
enums
factory
observers
strategies
payment_gateway_demo.py
main
payment_gateway_service.py
payment_gateway_demo.py
Output

6. Quiz

Design Payment Gateway Quiz

1 / 21
Multiple Choice

Which entity in a payment gateway design is responsible for encapsulating all details needed to start a payment?

How helpful was this article?

Comments (1)


0/2000
Sort by
The Cap16 days ago

Shouldn't all the processors implementing AbstractPaymentProcessor be singletons? I'm asking because they are stateless, and it might not be efficient to create new instances every time.

Copilot extension content script